Release 10.1A: OpenEdge Development:
Progress Dynamics Basic Development


Defining business logic for SBOs

The standard validation hooks for the SBO are basically the same as for the SDO: they have the names preTransactionValidate, beginTransactionValidate, endTransactionValidate, and postTransactionValidate. The preTransactionValidate procedure executes before the corresponding procedure in any contained SDO and can modify values in SDO records that will be seen by the validation logic in the SDOs. The beginTransactionValidate procedure executes at the very beginning of the single database transaction that encompasses all the updates in the SBO and its SDOs. Likewise, the endTransactionValidate procedure executes at the very end.

The dynamics SBO also supports a custom logic procedure that works just like the dynamic SDO’s custom logic procedure.

This first code example verifies that the client-side logic defined for an SDO will execute even when it is contained in an SBO. The Object Generator creates code for you in each SDO that verifies that all indexed fields have been entered. Because this code can be executed without returning to the server where the database is, it is placed into the client-side rowObjectValidate procedure, as shown:

/*------------------------------------------------------------------------- 
 Purpose:   Procedure used to validate RowObject record client-side 
 Parameters: <none> 
 Notes:    Other mandatory checks left out of this example. 
-------------------------------------------------------------------------*/ 
DEFINE VARIABLE cMessageList  AS CHARACTER  NO-UNDO. 
DEFINE VARIABLE cValueList   AS CHARACTER  NO-UNDO. 
 IF isFieldBlank(b_Order.SalesRep) THEN 
  ASSIGN 
   cMessageList = cMessageList +  
    (IF NUM-ENTRIES(cMessageList,CHR(3)) > 0 THEN   CHR(3) ELSE '':U) +  
    {aferrortxt.i 'AF' '1' 'Order' 'SalesRep' "'Sales Rep'"}. 
 ERROR-STATUS:ERROR = NO. 
 RETURN cMessageList. 
END PROCEDURE. 

If you run the ordersbowin window, delete the SalesRep code from an Order, then choose Save, you immediately see the Progress Dynamics message shown in Figure 11–10.

Figure 11–10: ADM2 message

Because the logic executes on the client, you see the error as soon as you choose Save, without having to do a commit.

The next code example shows that the SDO’s preTransactionValidate or writePreTransValidate logic will be executed from inside an SBO. The following excerpt from the writePreTransValidate procedure generated for the Order SDO by the Object Builder verifies that an updated OrderNum field does not match an existing OrderNum in the database:

/*------------------------------------------------------------------------- 
 Purpose:   Procedure used to validate records server-side before the  
        transaction scope upon write 
 Parameters: <none> 
 Notes:     
-------------------------------------------------------------------------*/ 
 DEFINE VARIABLE cMessageList  AS CHARACTER  NO-UNDO. 
 DEFINE VARIABLE cValueList   AS CHARACTER  NO-UNDO. 
 IF NOT isCreate() AND CAN-FIND(FIRST Order  
       WHERE Order.CustNum = b_Order.CustNum 
        AND Order.Ordernum = b_Order.Ordernum 
        AND ROWID(Order) <> TO-ROWID(ENTRY(1,b_Order.RowIDent))) THEN 
 DO: 
   ASSIGN 
    cValueList  = STRING(b_Order.CustNum) + ', ' + STRING(b_Order.Ordernum) 
    cMessageList = cMessageList +  
     (IF NUM-ENTRIES(cMessageList,CHR(3)) > 0 THEN CHR(3) ELSE '':U) +  
      {aferrortxt.i 'AF' '8' 'Order' '' "'CustNum, Ordernum, '" cValueList }. 
 END. 
ERROR-STATUS:ERROR = NO. 
 RETURN cMessageList. 
END PROCEDURE. 

For example, if you run the SBO window and change OrderNum from 1 to 2, then choose Save, and then Commit, the error message shown in Figure 11–11 appears.

Figure 11–11: ADM2 error message

Because this logic must execute on the server side, you do not see the error when you first choose Save. At that time, the change is saved only locally on the client. Only when you choose Commit to send the change back to the server does the writePreTransValidate execute. The code formats the error message and returns it up the execution stack, where it is intercepted by the framework code and returned to the client for display.

The next code example shows the interaction between the SBO preTransactionValidate and its counterpart in the SDO. In this rather contrived bit of code, the procedure first checks that if the PO field is not blank, then it must begin with the letters PO. If not, the code returns an error. Otherwise, if it is blank, it is replaced by the string <none>. In the SBO, each contained SDO’s temp-table has, by default, the unqualified name of the SDO itself. You can change this value in the Instance Property dialog box for the SDOs within the SBO, as shown in the following example:

/*------------------------------------------------------------------------- 
 Purpose:  Verify that a PO begins with the letters 'PO'. 
       If it's blank then set it to <none>.  
 Parameters: <none> 
 Notes:     
-------------------------------------------------------------------------*/ 
FOR EACH orderfullo WHERE orderfullo.RowMod = "U":U: 
  IF orderfullo.PO NE "":U AND NOT orderfullo.PO BEGINS "PO":U THEN 
   RETURN {aferrortxt.i '?' "'PO must begin with PO'" 'Order' '' 'PO'  
    orderfullo.PO }. 
  ELSE IF orderfullo.PO = "":U THEN 
    orderfullo.PO = "<none>":U. 
END. 
END PROCEDURE. 

If you run the application again with this code, then save and commit any change to an Order record with a blank PO, the value is changed to <none>. This value is returned to the client and displayed after the change is committed.

The next code sample adds another check to the Order SDO’s writePreTransValidate procedure. As described in Chapter 10, "Building Basic Business Logic in a Progress Dynamics Application," if you use the create/write/delete validation procedures in the SDO logic procedure, the buffer name is the simple table name preceded by b_, as shown:

IF b_order.PO = "<none>":U THEN 
   cMessageList = cMessageList + (IF NUM-ENTRIES(cMessageList,CHR(3)) > 0  
    THEN CHR(3) ELSE '':U) +  
    {aferrortxt.i '?' "'PO cannot equal <none>'" 'Order' '' 'PO' cValueList }. 

This code verifies that the PO number field has not been set to the string <none>. If it has, then an error is reported. This example does not put this error into the Progress Dynamics message table as recommended you always do. For this reason, the message text is just inserted into the message formatting include file aferrortxt.i.

Because the SBO’s preTransactionValidate procedure executes first, it changes a blank PO number to ‘<none>’, and passes this along to the SDO for further validation. Thus a blank PO is rejected with the error message that it cannot equal <none>!

The next block of code shows how the SBO can look at updates in more than one SDO, referring to each by its logical name. This logic totals the quantity of any modified OrderLine records of each Order record, which was itself modified. It puts that total into the Order record’s PO field, so that the calculation can be seen back on the client when the transaction completes, as shown:

/* Additional code for SBO preTransactionValidate: */ 
DEFINE VARIABLE iQty AS INTEGER NO-UNDO. 
/* If both an Order and OrderLine records were modified, total the quantities 
  of the changed OrderLine records and stick that (in brackets) into the  
  PO fld. */ 
 FOR EACH orderfullo WHERE orderfullo.rowmod = 'u': 
   FOR EACH orderlinfullo WHERE orderlinfullo.rowmod = 'u' AND  
     orderlinfullo.ordernum = orderfullo.ordernum: 
       iQty = iQty + orderlinfullo.Qty. 
   END. 
   IF iQty NE 0 THEN  
     orderfullo.PO = orderfullo.PO + " <" + STRING(iQty) + ">". 
 END. 

The next example is a beginTransactionValidate procedure for the SBO. This is the place to put logic that must lock or modify other database records before the SDOs’ updates are written back to the database. This simple example identifies each OrderLine where the Price field has been modified, finds the corresponding Item record in the database, and modifies that record’s Price field to match. (This almost certainly is not what real-world business logic would want to do, but it serves the purpose of a demonstration.) The Item record is updated as part of the same transaction as the OrderLine changes, as shown:

PROCEDURE beginTransactionValidate: 
/*---------------------------------------------------------------------- 
 Purpose:   SBO logic to execute at the beginning of the transaction. 
 Parameters: <none> 
----------------------------------------------------------------------*/ 
  DEFINE BUFFER doline2 FOR orderlinfullo. 
  /* Set the price for the item record in the database equal to the 
   price in the OrderLine record in the SDO if it has been changed.  
   The Doline2 buffer allows us to compare the Before and After 
   versions of each modified OrderLine. */ 
  FOR EACH orderlinfullo WHERE orderlinfullo.rowmod = 'u': 
   FIND doline2 WHERE doline2.rownum = orderlinfullo.rownum AND  
     doline2.rowmod = ''. 
   IF orderlinfullo.price NE doline2.price THEN 
   DO: 
     FIND ITEM OF orderlinfullo NO-ERROR. 
     IF NOT AVAILABLE(ITEM) THEN 
       RETURN "Item Record for OrderLine " +  
        STRING(orderlinfullo.ordernum) 
        + " " + STRING(orderlinfullo.linenum) + " not found.". 
     ELSE ITEM.price = orderlinfullo.price. 
   END. 
  END. 
END PROCEDURE. 

Notes: As an alternative to defining a second buffer for the Before version of a changed record, as the previous example did, if you only need to know which fields were modified, you can look at the ChangedFields field of the update table record. If the condition is true (the Order and the Price or Qty of at least one of its OrderLine records have been changed), then all the OrderLine records for the Order are re-read from the database, totaled, and the total placed into the PO field of the Order.

This kind of logic has to take place at the end of the transaction, because both modified OrderLine records and unmodified OrderLine records must be read. You want the total to reflect both updates that were just made within the transaction, and any other OrderLine records that were also in the database but not modified in this transaction.

Finally, you define an endTransactionValidate procedure for the SBO. This is the place to put logic that must execute within the transaction block, but after all of the SDO updates have taken place. The procedure determines if an Order record has been changed, and if the Qty or Price field for at least one of its OrderLine records has also changed.

In addition, because it is actually the ExtendedPrice that is being totaled, and the ExtendedPrice is calculated by a database trigger, this value will be available only at the end of the transaction, after the individual OrderLine records have been written to the database.

To demonstrate returning an error from within the transaction, the following code example then checks the CreditLimit of the Customer record associated with the updated Order. It returns an error message if the CreditLimit has been exceeded by this new Order total. If this happens, the whole transaction is backed out, the error message will be returned to the client, and the user will be able to make whatever changes are necessary to be able to resubmit the updates successfully. Or the user can back out of all the changes to all SDOs by choosing the Undo button, as shown:

PROCEDURE endTransactionValidate: 
/*---------------------------------------------------------------------- 
 Purpose:   SBO logic to be executed at the end of the transaction. 
 Parameters: <none> 
----------------------------------------------------------------------*/ 
DEFINE VARIABLE dPrice AS DECIMAL  NO-UNDO. 
 FOR EACH orderfullo WHERE orderfullo.rowmod = 'u': 
   /* Stick the order total in the order record, if the order 
     and at least one of its OrderLine records' price/qty was updated. */ 
   FIND FIRST orderlinfullo WHERE orderlinfullo.rowmod = 'u' AND 
     (LOOKUP('Qty', orderlinfullo.ChangedFields) NE 0 OR 
      LOOKUP('Price', orderlinfullo.ChangedFields) NE 0) NO-ERROR. 
   IF AVAILABLE(orderlinfullo) THEN 
   DO: 
     FOR EACH OrderLine WHERE OrderLine.OrderNum = orderfullo.orderNum: 
       dPrice = dPrice + OrderLine.ExtendedPrice. 
     END. 
     FIND Order WHERE Order.Ordernum = orderfullo.orderNum. 
     FIND Customer WHERE Customer.CustNum = Order.CustNum. 
     IF dprice > Customer.CreditLimit THEN 
       RETURN "This one order total of " + STRING(dPrice) + 
       " exceeds the Customer Credit Limit!". 
     ELSE ASSIGN Order.PO = Order.PO + " [" + STRING(dPrice) + "]" 
           orderfullo.PO = Order.PO. 
   END. 
 END. 
END PROCEDURE. 

After making some changes to the database with this logic, the Order records will start to look rather strange. The values in angle brackets (< >) are the totals of OrderLine quantities modified within a transaction. The values in square brackets ([ ]) are the Order totals, that is, the total of all ExtendedPrice values for all OrderLine records for an Order, whenever both the Order record and the Qty or Price fields of one or more of the OrderLine records have been modified within a single transaction.


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095